/*
  Copyright (C) 2024 Vasily Evseenko <svpcom@p2ptech.org>

  Based on example written by David Rheinsberg <david.rheinsberg@gmail.com>
  http://github.com/dvdhrm/docs

 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */


/*
 * Planes can be used to blend or overlay images on top of a CRTC
 * framebuffer during the scanout process. Not all hardware provide
 * planes and the number of planes available is also limited. If there's
 * not enough planes available or the hardware does not provide them,
 * users should fallback to composition via GPU or CPU to blend or
 * overlay the planes. Notice that this render process will result
 * in delay, what justifies the usage of planes by modern hardware
 * that needs to be fast.
 *
 * There are three types of planes: primary, cursor and overlay. For
 * compatibility with legacy userspace, the default behavior is to expose
 * only overlay planes to userspace (we're going to see in the code that
 * we have to ask to receive all types of planes). A good example of plane
 * usage is this: imagine a static desktop screen and the user is moving
 * the cursor around. Only the cursor is moving. Instead of calculating the
 * complete scene for each time the user moves its cursor, we can update only
 * the cursor plane and it will be automatically overlayed by the hardware on
 * top of the primary plane. There's no need of software composition in this
 * case.
 *
 * But there was synchronisation problems related to multiple planes
 * usage. The KMS API was not atomic, so you'd have to update the primary
 * plane and then the overlay planes with distinct IOCTL's. This could lead
 * to tearing and also some trouble related to blocking, so the atomic
 * API was proposed to fix these problems.
 *
 * With the introduction of the KMS atomic API, all the planes could get
 * updated in a single IOCTL, using drmModeAtomicCommit(). This can be
 * either asynchronous or fully blocking.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <drm_fourcc.h>

#include "graphengine.h"

#define FB_WIDTH  GRAPHICS_WIDTH
#define FB_HEIGHT GRAPHICS_HEIGHT
#define ZPOS 7

/*
 * A new struct is introduced: drm_object. It stores properties of certain
 * objects (connectors, CRTC and planes) that are used in atomic modeset setup
 * and also in atomic page-flips (all planes updated in a single IOCTL).
 */

struct drm_object {
    drmModeObjectProperties *props;
    drmModePropertyRes **props_info;
    uint32_t id;
};

struct modeset_buf {
    uint32_t width;
    uint32_t height;
    uint32_t stride;
    uint32_t size;
    uint32_t handle;
    uint8_t *map;
    uint32_t fb;
};

struct modeset_output {
    struct modeset_output *next;

    unsigned int front_buf;
    struct modeset_buf bufs[2];

    struct drm_object connector;
    struct drm_object crtc;
    struct drm_object plane;

    drmModeModeInfo mode;
    uint32_t mode_blob_id;
    uint32_t crtc_index;
};

static struct modeset_output *output_list = NULL;

/*
 * modeset_open() changes just a little bit. We now have to set that we're going
 * to use the KMS atomic API and check if the device is capable of handling it.
 */

static int modeset_open(int *out, const char *node)
{
    int fd, ret;
    uint64_t cap;

    fd = open(node, O_RDWR | O_CLOEXEC);
    if (fd < 0) {
        ret = -errno;
        fprintf(stderr, "cannot open '%s': %m\n", node);
        return ret;
    }

    /* Set that we want to receive all the types of planes in the list. This
     * have to be done since, for legacy reasons, the default behavior is to
     * expose only the overlay planes to the users. The atomic API only
     * works if this is set.
     */
    ret = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
    if (ret) {
        fprintf(stderr, "failed to set universal planes cap, %d\n", ret);
        return ret;
    }

    /* Here we set that we're going to use the KMS atomic API. It's supposed
     * to set the DRM_CLIENT_CAP_UNIVERSAL_PLANES automatically, but it's a
     * safe behavior to set it explicitly as we did in the previous
     * commands. This is also good for learning purposes.
     */
    ret = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);
    if (ret) {
        fprintf(stderr, "failed to set atomic cap, %d", ret);
        return ret;
    }

    if (drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &cap) < 0 || !cap) {
        fprintf(stderr, "drm device '%s' does not support dumb buffers\n",
                node);
        close(fd);
        return -EOPNOTSUPP;
    }

    if (drmGetCap(fd, DRM_CAP_CRTC_IN_VBLANK_EVENT, &cap) < 0 || !cap) {
        fprintf(stderr, "drm device '%s' does not support atomic KMS\n",
                node);
        close(fd);
        return -EOPNOTSUPP;
    }

    *out = fd;
    return 0;
}

/*
 * get_property_value() is a new function. Given a device, the properties of
 * an object and a name, search for the value of property 'name'. If we can't
 * find it, return -1.
 */

static int64_t get_property_value(int fd, drmModeObjectPropertiesPtr props,
				  const char *name)
{
    drmModePropertyPtr prop;
    uint64_t value;
    bool found;
    int j;

    found = false;
    for (j = 0; j < props->count_props && !found; j++) {
        prop = drmModeGetProperty(fd, props->props[j]);
        if (!strcmp(prop->name, name)) {
            value = props->prop_values[j];
            found = true;
        }
        drmModeFreeProperty(prop);
    }

    if (!found)
        return -1;
    return value;
}

/*
 * get_drm_object_properties() is a new helpfer function that retrieves
 * the properties of a certain CRTC, plane or connector object.
 */

static void modeset_get_object_properties(int fd, struct drm_object *obj,
					  uint32_t type)
{
    const char *type_str;
    unsigned int i;

    obj->props = drmModeObjectGetProperties(fd, obj->id, type);
    if (!obj->props) {
        switch(type) {
        case DRM_MODE_OBJECT_CONNECTOR:
            type_str = "connector";
            break;
        case DRM_MODE_OBJECT_PLANE:
            type_str = "plane";
            break;
        case DRM_MODE_OBJECT_CRTC:
            type_str = "CRTC";
            break;
        default:
            type_str = "unknown type";
            break;
        }
        fprintf(stderr, "cannot get %s %d properties: %s\n",
                type_str, obj->id, strerror(errno));
        return;
    }

    obj->props_info = calloc(obj->props->count_props, sizeof(obj->props_info));
    for (i = 0; i < obj->props->count_props; i++)
        obj->props_info[i] = drmModeGetProperty(fd, obj->props->props[i]);
}

/*
 * set_drm_object_property() is a new function. It sets a property value to a
 * CRTC, plane or connector object.
 */

static int set_drm_object_property(drmModeAtomicReq *req, struct drm_object *obj,
				   const char *name, uint64_t value)
{
    int i;
    uint32_t prop_id = 0;

    for (i = 0; i < obj->props->count_props; i++) {
        if (!strcmp(obj->props_info[i]->name, name)) {
            prop_id = obj->props_info[i]->prop_id;
            break;
        }
    }

    if (prop_id == 0) {
        fprintf(stderr, "no object property: %s\n", name);
        return -EINVAL;
    }

    return drmModeAtomicAddProperty(req, obj->id, prop_id, value);
}

/*
 * modeset_find_crtc() changes a little bit. Now we also have to save the CRTC
 * index, and not only its id.
 */

static int modeset_find_crtc(int fd, drmModeRes *res, drmModeConnector *conn,
			     struct modeset_output *out)
{
    drmModeEncoder *enc;
    unsigned int i, j;
    uint32_t crtc;
    struct modeset_output *iter;

    /* first try the currently conected encoder+crtc */
    if (conn->encoder_id)
        enc = drmModeGetEncoder(fd, conn->encoder_id);
    else
        enc = NULL;

    if (enc) {
        if (enc->crtc_id) {
            crtc = enc->crtc_id;
            for (iter = output_list; iter; iter = iter->next) {
                if (iter->crtc.id == crtc) {
                    crtc = 0;
                    break;
                }
            }

            if (crtc > 0) {
                drmModeFreeEncoder(enc);
                out->crtc.id = crtc;
                /* find the CRTC's index */
                for (i = 0; i < res->count_crtcs; ++i) {
                    if (res->crtcs[i] == crtc) {
                        out->crtc_index = i;
                        break;
                    }
                }
                return 0;
            }
        }

        drmModeFreeEncoder(enc);
    }

    /* If the connector is not currently bound to an encoder or if the
     * encoder+crtc is already used by another connector (actually unlikely
     * but lets be safe), iterate all other available encoders to find a
     * matching CRTC.
     */
    for (i = 0; i < conn->count_encoders; ++i) {
        enc = drmModeGetEncoder(fd, conn->encoders[i]);
        if (!enc) {
            fprintf(stderr, "cannot retrieve encoder %u:%u (%d): %m\n",
                    i, conn->encoders[i], errno);
            continue;
        }

        /* iterate all global CRTCs */
        for (j = 0; j < res->count_crtcs; ++j) {
            /* check whether this CRTC works with the encoder */
            if (!(enc->possible_crtcs & (1 << j)))
                continue;

            /* check that no other output already uses this CRTC */
            crtc = res->crtcs[j];
            for (iter = output_list; iter; iter = iter->next) {
                if (iter->crtc.id == crtc) {
                    crtc = 0;
                    break;
                }
            }

            /* We have found a CRTC, so save it and return. Note
             * that we have to save its index as well. The CRTC
             * index (not its ID) will be used when searching for a
             * suitable plane.
             */
            if (crtc > 0) {
                fprintf(stdout, "crtc %u found for encoder %u, will need full modeset\n",
                        crtc, conn->encoders[i]);;
                drmModeFreeEncoder(enc);
                out->crtc.id = crtc;
                out->crtc_index = j;
                return 0;
            }
        }

        drmModeFreeEncoder(enc);
    }

    fprintf(stderr, "cannot find suitable crtc for connector %u\n",
            conn->connector_id);
    return -ENOENT;
}

/*
 * modeset_find_plane() is a new function. Given a certain combination
 * of connector+CRTC, it looks for a primary plane for it.
 */

static int modeset_find_plane(int fd, struct modeset_output *out)
{
    drmModePlaneResPtr plane_res;
    bool found_primary = false;
    int i, ret = -EINVAL;

    plane_res = drmModeGetPlaneResources(fd);
    if (!plane_res) {
        fprintf(stderr, "drmModeGetPlaneResources failed: %s\n",
                strerror(errno));
        return -ENOENT;
    }

    /* iterates through all planes of a certain device */
    for (i = 0; (i < plane_res->count_planes) && !found_primary; i++) {
        int plane_id = plane_res->planes[i];

        drmModePlanePtr plane = drmModeGetPlane(fd, plane_id);
        if (!plane) {
            fprintf(stderr, "drmModeGetPlane(%u) failed: %s\n", plane_id,
                    strerror(errno));
            continue;
        }

        /* check if the plane can be used by our CRTC */
        if (plane->possible_crtcs & (1 << out->crtc_index)) {
            drmModeObjectPropertiesPtr props =
                drmModeObjectGetProperties(fd, plane_id, DRM_MODE_OBJECT_PLANE);

            /* Get the "type" property to check if this is a primary
             * plane. Type property is special, as its enum value is
             * defined in UAPI headers. For the properties that are
             * not defined in the UAPI headers, we would have to
             * give kernel the property name and it would return the
             * corresponding enum value. We could also do this for
             * the "type" property, but it would make this simple
             * example more complex. The reason why defining enum
             * values for kernel properties in UAPI headers is
             * deprecated is that string names are easier to both
             * (userspace and kernel) make unique and keep
             * consistent between drivers and kernel versions. But
             * in order to not break userspace, some properties were
             * left in the UAPI headers as well.
             */
            if (get_property_value(fd, props, "type") == DRM_PLANE_TYPE_PRIMARY) {
                found_primary = true;
                out->plane.id = plane_id;
                ret = 0;
            }

            drmModeFreeObjectProperties(props);
        }

        drmModeFreePlane(plane);
    }

    drmModeFreePlaneResources(plane_res);

    if (found_primary)
        fprintf(stdout, "found primary plane, id: %d\n", out->plane.id);
    else
        fprintf(stdout, "couldn't find a primary plane\n");
    return ret;
}

/*
 * modeset_drm_object_fini() is a new helper function that destroys CRTCs,
 * connectors and planes
 */

static void modeset_drm_object_fini(struct drm_object *obj)
{
    for (int i = 0; i < obj->props->count_props; i++)
        drmModeFreeProperty(obj->props_info[i]);
    free(obj->props_info);
    drmModeFreeObjectProperties(obj->props);
}

/*
 * modeset_setup_objects() is a new function. It helps us to retrieve
 * connector, CRTC and plane objects properties from the device. These
 * properties will help us during the atomic modesetting commit, so we save
 * them in our struct modeset_output object.
 */

static int modeset_setup_objects(int fd, struct modeset_output *out)
{
    struct drm_object *connector = &out->connector;
    struct drm_object *crtc = &out->crtc;
    struct drm_object *plane = &out->plane;

    /* retrieve connector properties from the device */
    modeset_get_object_properties(fd, connector, DRM_MODE_OBJECT_CONNECTOR);
    if (!connector->props)
        goto out_conn;

    /* retrieve CRTC properties from the device */
    modeset_get_object_properties(fd, crtc, DRM_MODE_OBJECT_CRTC);
    if (!crtc->props)
        goto out_crtc;

    /* retrieve plane properties from the device */
    modeset_get_object_properties(fd, plane, DRM_MODE_OBJECT_PLANE);
    if (!plane->props)
        goto out_plane;

    return 0;

out_plane:
    modeset_drm_object_fini(crtc);
out_crtc:
    modeset_drm_object_fini(connector);
out_conn:
    return -ENOMEM;
}

/*
 * modeset_destroy_objects() is a new function. It destroys what we allocate
 * in modeset_setup_objects().
 */

static void modeset_destroy_objects(int fd, struct modeset_output *out)
{
    modeset_drm_object_fini(&out->connector);
    modeset_drm_object_fini(&out->crtc);
    modeset_drm_object_fini(&out->plane);
}

/*
 * modeset_create_fb() stays the same.
 */

static int modeset_create_fb(int fd, struct modeset_buf *buf)
{
    struct drm_mode_create_dumb creq;
    struct drm_mode_destroy_dumb dreq;
    struct drm_mode_map_dumb mreq;
    int ret;
    uint32_t handles[4] = {0}, pitches[4] = {0}, offsets[4] = {0};

    /* create dumb buffer */
    memset(&creq, 0, sizeof(creq));
    creq.width = buf->width;
    creq.height = buf->height;
    creq.bpp = 32;
    ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq);
    if (ret < 0) {
        fprintf(stderr, "cannot create dumb buffer (%d): %m\n",
                errno);
        return -errno;
    }
    buf->stride = creq.pitch;
    buf->size = creq.size;
    buf->handle = creq.handle;

    /* create framebuffer object for the dumb-buffer */
    handles[0] = buf->handle;
    pitches[0] = buf->stride;
    ret = drmModeAddFB2(fd, buf->width, buf->height, DRM_FORMAT_ABGR8888,
                        handles, pitches, offsets, &buf->fb, 0);
    if (ret) {
        fprintf(stderr, "cannot create framebuffer (%d): %m\n",
                errno);
        ret = -errno;
        goto err_destroy;
    }

    /* prepare buffer for memory mapping */
    memset(&mreq, 0, sizeof(mreq));
    mreq.handle = buf->handle;
    ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);
    if (ret) {
        fprintf(stderr, "cannot map dumb buffer (%d): %m\n",
                errno);
        ret = -errno;
        goto err_fb;
    }

    /* perform actual memory mapping */
    buf->map = mmap(0, buf->size, PROT_READ | PROT_WRITE, MAP_SHARED,
                    fd, mreq.offset);
    if (buf->map == MAP_FAILED) {
        fprintf(stderr, "cannot mmap dumb buffer (%d): %m\n",
                errno);
        ret = -errno;
        goto err_fb;
    }

    /* clear the framebuffer to 0 */
    memset(buf->map, 0, buf->size);

    return 0;

err_fb:
    drmModeRmFB(fd, buf->fb);
err_destroy:
    memset(&dreq, 0, sizeof(dreq));
    dreq.handle = buf->handle;
    drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
    return ret;
}

/*
 * modeset_destroy_fb() stays the same.
 */

static void modeset_destroy_fb(int fd, struct modeset_buf *buf)
{
    struct drm_mode_destroy_dumb dreq;

    /* unmap buffer */
    munmap(buf->map, buf->size);

    /* delete framebuffer */
    drmModeRmFB(fd, buf->fb);

    /* delete dumb buffer */
    memset(&dreq, 0, sizeof(dreq));
    dreq.handle = buf->handle;
    drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq);
}

/*
 * modeset_setup_framebuffers() creates framebuffers for the back and front
 * buffers of a certain output. Also, it copies the connector mode to these
 * buffers.
 */

static int modeset_setup_framebuffers(int fd,
				      struct modeset_output *out)
{
    int i, ret;

    /* setup the front and back framebuffers */
    for (i = 0; i < 2; i++) {

        /* copy mode info to buffer */
        out->bufs[i].width = FB_WIDTH;
        out->bufs[i].height = FB_HEIGHT;

        /* create a framebuffer for the buffer */
        ret = modeset_create_fb(fd, &out->bufs[i]);
        if (ret) {
            /* the second framebuffer creation failed, so
             * we have to destroy the first before returning */
            if (i == 1)
                modeset_destroy_fb(fd, &out->bufs[0]);
            return ret;
        }
    }

    return 0;
}

/*
 * modeset_output_destroy() is new. It destroys the objects (connector, crtc and
 * plane), front and back buffers, the mode blob property and then destroys the
 * output itself.
 */

static void modeset_output_destroy(int fd, struct modeset_output *out)
{
    /* destroy connector, crtc and plane objects */
    modeset_destroy_objects(fd, out);

    /* destroy front/back framebuffers */
    modeset_destroy_fb(fd, &out->bufs[0]);
    modeset_destroy_fb(fd, &out->bufs[1]);

    /* destroy mode blob property */
    drmModeDestroyPropertyBlob(fd, out->mode_blob_id);

    free(out);
}

/*
 * With a certain combination of connector+CRTC, we look for a suitable primary
 * plane for it. After that, we retrieve connector, CRTC and plane objects
 * properties from the device. These objects are used during the atomic modeset
 * setup (see modeset_atomic_prepare_commit()) and also during the page-flips
 * (see modeset_draw_commit() and modeset_atomic_commit()).
 *
 * Besides that, we have to create a blob property that receives the output
 * mode. When we perform an atomic commit, the driver expects a CRTC property
 * named "MODE_ID", which points to the id of a blob. This usually happens for
 * properties that are not simple types. In this particular case, out->mode is a
 * struct. But we could have another property that expects the id of a blob that
 * holds an array, for instance.
 */

static struct modeset_output *modeset_output_create(int fd, drmModeRes *res,
						    drmModeConnector *conn)
{
    int ret;
    struct modeset_output *out;

    /* creates an output structure */
    out = malloc(sizeof(*out));
    memset(out, 0, sizeof(*out));
    out->connector.id = conn->connector_id;

    /* check if a monitor is connected */
    if (conn->connection != DRM_MODE_CONNECTED) {
        fprintf(stderr, "ignoring unused connector %u\n",
                conn->connector_id);
        goto out_error;
    }

    /* check if there is at least one valid mode */
    if (conn->count_modes == 0) {
        fprintf(stderr, "no valid mode for connector %u\n",
                conn->connector_id);
        goto out_error;
    }

    /* copy the mode information into our output structure */
    memcpy(&out->mode, &conn->modes[0], sizeof(out->mode));
    /* create the blob property using out->mode and save its id in the output*/
    if (drmModeCreatePropertyBlob(fd, &out->mode, sizeof(out->mode),
                                  &out->mode_blob_id) != 0) {
        fprintf(stderr, "couldn't create a blob property\n");
        goto out_error;
    }
    fprintf(stderr, "mode for connector %u is %ux%u\n",
            conn->connector_id, out->bufs[0].width, out->bufs[0].height);

    /* find a crtc for this connector */
    ret = modeset_find_crtc(fd, res, conn, out);
    if (ret) {
        fprintf(stderr, "no valid crtc for connector %u\n",
                conn->connector_id);
        goto out_blob;
    }

    /* with a connector and crtc, find a primary plane */
    ret = modeset_find_plane(fd, out);
    if (ret) {
        fprintf(stderr, "no valid plane for crtc %u\n", out->crtc.id);
        goto out_blob;
    }

    /* gather properties of our connector, CRTC and planes */
    ret = modeset_setup_objects(fd, out);
    if (ret) {
        fprintf(stderr, "cannot get plane properties\n");
        goto out_blob;
    }

    /* setup front/back framebuffers for this CRTC */
    ret = modeset_setup_framebuffers(fd, out);
    if (ret) {
        fprintf(stderr, "cannot create framebuffers for connector %u\n",
                conn->connector_id);
        goto out_obj;
    }

    return out;

out_obj:
    modeset_destroy_objects(fd, out);
out_blob:
    drmModeDestroyPropertyBlob(fd, out->mode_blob_id);
out_error:
    free(out);
    return NULL;
}

/*
 * modeset_prepare() changes a little bit. Now we use the new function
 * modeset_output_create() to allocate memory and setup the output.
 */

static int modeset_prepare(int fd)
{
    drmModeRes *res;
    drmModeConnector *conn;
    unsigned int i;
    struct modeset_output *out;

    /* retrieve resources */
    res = drmModeGetResources(fd);
    if (!res) {
        fprintf(stderr, "cannot retrieve DRM resources (%d): %m\n",
                errno);
        return -errno;
    }

    /* iterate all connectors */
    for (i = 0; i < res->count_connectors; ++i) {
        /* get information for each connector */
        conn = drmModeGetConnector(fd, res->connectors[i]);
        if (!conn) {
            fprintf(stderr, "cannot retrieve DRM connector %u:%u (%d): %m\n",
                    i, res->connectors[i], errno);
            continue;
        }

        /* create an output structure and free connector data */
        out = modeset_output_create(fd, res, conn);
        drmModeFreeConnector(conn);
        if (!out)
            continue;

        /* link output into global list */
        out->next = output_list;
        output_list = out;
    }
    if (!output_list) {
        fprintf(stderr, "couldn't create any outputs\n");
        return -1;
    }

    /* free resources again */
    drmModeFreeResources(res);
    return 0;
}

/*
 * modeset_atomic_prepare_commit() is new. Here we set the values of properties
 * (of our connector, CRTC and plane objects) that we want to change in the
 * atomic commit. These changes are temporarily stored in drmModeAtomicReq *req
 * until the commit actually happens.
 */

static int modeset_atomic_prepare_commit(int fd, struct modeset_output *out,
					 drmModeAtomicReq *req)
{
    struct drm_object *plane = &out->plane;
    struct modeset_buf *buf = &out->bufs[out->front_buf ^ 1];

    /* set id of the CRTC id that the connector is using */
    if (set_drm_object_property(req, &out->connector, "CRTC_ID", out->crtc.id) < 0)
        return -1;

    /* set the mode id of the CRTC; this property receives the id of a blob
     * property that holds the struct that actually contains the mode info */
    if (set_drm_object_property(req, &out->crtc, "MODE_ID", out->mode_blob_id) < 0)
        return -1;

    /* set the CRTC object as active */
    if (set_drm_object_property(req, &out->crtc, "ACTIVE", 1) < 0)
        return -1;

    /* set properties of the plane related to the CRTC and the framebuffer */
    if (set_drm_object_property(req, plane, "FB_ID", buf->fb) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "CRTC_ID", out->crtc.id) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "SRC_X", 0) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "SRC_Y", 0) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "SRC_W", buf->width << 16) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "SRC_H", buf->height << 16) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "CRTC_X", 0) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "CRTC_Y", 0) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "CRTC_W", out->mode.hdisplay) < 0)
        return -1;
    if (set_drm_object_property(req, plane, "CRTC_H", out->mode.vdisplay) < 0)
        return -1;

    return 0;
}

/*
 * Draw on back framebuffer before the page-flip is requested.
 */

static void modeset_paint_framebuffer(struct modeset_output *out, uint32_t color)
{
    struct modeset_buf *buf;

    buf = &out->bufs[out->front_buf ^ 1];
    for (int j = 0; j < buf->height; ++j) {
        for (int k = 0; k < buf->width; ++k) {
            int off = buf->stride * j + k * 4;
            *(uint32_t*)(buf->map + off) = color;
        }
    }
}

/*
 * modeset_draw_commit() prepares the framebuffer with the drawing and then it asks
 * for the driver to perform an atomic commit. This will lead to a page-flip and
 * the content of the framebuffer will be displayed. In this simple example
 * we're only using the primary plane, but we could also be updating other
 * planes in the same atomic commit.
 *
 * Just like in modeset_perform_modeset(), we first setup everything with
 * modeset_atomic_prepare_commit() and then actually perform the atomic commit.
 * But there are some important differences:
 *
 * 1. Here we just want to perform a commit that changes the state of a specific
 *    output, and in modeset_perform_modeset() we did an atomic commit that was
 *    supposed to setup all the outputs at once. So there's no need to prepare
 *    every output before performing the atomic commit. But let's suppose you
 *    prepare every output and then perform the commit. It should schedule a
 *    page-flip for all of them, but modeset_draw_commit() was called because the
 *    page-flip for a specific output has finished. The others may not be
 *    prepared for a page-flip yet (e.g. in the middle of a scanout), so these
 *    page-flips will fail.
 *
 * 2. Here we have already painted the framebuffer and also we don't use the
 *    flag DRM_MODE_ALLOW_MODESET anymore, since the modeset already happened.
 *    We could continue to use this flag, as it makes no difference if
 *    modeset_perform_modeset() is correct and there's no bug in the kernel.
 *    The flag only allows (it doesn't force) the driver to perform a modeset,
 *    but we have already performed it in modeset_perform_modeset() and now we
 *    just want page-flips to occur. If we still need to perform modesets it
 *    means that we have a bug somewhere, and it may be better to fail than to
 *    glitch (a modeset can cause unecessary latency and also blank the screen).
 */

static void modeset_draw_commit(int fd, struct modeset_output *out)
{
    drmModeAtomicReq *req;
    int ret, flags;

    /* prepare output for atomic commit */
    req = drmModeAtomicAlloc();
    ret = modeset_atomic_prepare_commit(fd, out, req);
    if (ret < 0) {
        fprintf(stderr, "prepare atomic commit failed, %d\n", errno);
        return;
    }

    /* We've just draw on the framebuffer, prepared the commit and now it's
     * time to perform a page-flip to display its content.
     *
     * DRM_MODE_ATOMIC_NONBLOCK makes the page-flip non-blocking. We don't
     * want to be blocked waiting for the commit to happen, since we can use
     * this time to prepare a new framebuffer, for instance. We can only do
     * this because there are mechanisms to know when the commit is complete
     * (like page flip event, explained above).
     */
    flags = DRM_MODE_ATOMIC_NONBLOCK;
    ret = drmModeAtomicCommit(fd, req, flags, NULL);
    drmModeAtomicFree(req);

    if (ret < 0) {
        fprintf(stderr, "atomic commit failed, %d\n", errno);
        return;
    }

    out->front_buf ^= 1;
}


/*
 * modeset_perform_modeset() is new. First we define what properties have to be
 * changed and the values that they will receive. To check if the modeset will
 * work as expected, we perform an atomic commit with the flag
 * DRM_MODE_ATOMIC_TEST_ONLY. With this flag the DRM driver tests if the atomic
 * commit would work, but it doesn't commit it to the hardware. After, the same
 * atomic commit is performed without the TEST_ONLY flag, but not only before we
 * draw on the framebuffers of the outputs. This is necessary to avoid
 * displaying unwanted content.
 *
 * NOTE: we can't perform an atomic commit without an attached frambeuffer
 * (even when we have DRM_MODE_ATOMIC_TEST_ONLY). It will simply fail.
 */

static int modeset_perform_modeset(int fd)
{
    int ret, flags;
    struct modeset_output *iter;
    drmModeAtomicReq *req;

    /* prepare modeset on all outputs */
    req = drmModeAtomicAlloc();
    for (iter = output_list; iter; iter = iter->next) {
        ret = modeset_atomic_prepare_commit(fd, iter, req);
        if (ret < 0)
            break;
    }
    if (ret < 0) {
        fprintf(stderr, "prepare atomic commit failed, %d\n", errno);
        return ret;
    }

    /* perform test-only atomic commit */
    flags = DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET;
    ret = drmModeAtomicCommit(fd, req, flags, NULL);
    if (ret < 0) {
        fprintf(stderr, "test-only atomic commit failed, %d\n", errno);
        drmModeAtomicFree(req);
        return ret;
    }

    /* draw on back framebuffer of all outputs */
    for (iter = output_list; iter; iter = iter->next)
    {
        if (set_drm_object_property(req, &iter->plane, "zpos", ZPOS) < 0)
        {
            fprintf(stderr, "Unable to set zpos %d for primary plane\n", ZPOS);
            drmModeAtomicFree(req);
            return -1;
        }
        modeset_paint_framebuffer(iter, 0);
    }

    /* initial modeset on all outputs */
    flags = DRM_MODE_ATOMIC_ALLOW_MODESET;
    ret = drmModeAtomicCommit(fd, req, flags, NULL);
    if (ret < 0)
    {
        fprintf(stderr, "modeset atomic commit failed, %d\n", errno);
    }

    drmModeAtomicFree(req);

    return ret;
}

static void modeset_cleanup(int fd)
{
    struct modeset_output *iter;
    drmModeAtomicReq *req;
    int ret = 0;

    /* restore zpos on all outputs */
    req = drmModeAtomicAlloc();

    for(iter = output_list; iter; iter = iter->next)
    {
        set_drm_object_property(req, &iter->plane, "zpos", 0);
    }

    ret = drmModeAtomicCommit(fd, req, 0, NULL);

    if (ret < 0)
    {
        fprintf(stderr, "Unable to restore zpos for primary plane\n");
    }

    drmModeAtomicFree(req);

    while (output_list)
    {
        /* get first output from list */
        iter = output_list;

        /* move head of the list to the next output */
        output_list = iter->next;

        /* destroy current output */
        modeset_output_destroy(fd, iter);
    }
}

static int drm_fd = -1;

void drm_cleanup(void)
{
    /* cleanup everything */
    modeset_cleanup(drm_fd);
    close(drm_fd);
}

int drm_init(void)
{
    int ret;
    const char *card = "/dev/dri/card0";

    fprintf(stderr, "DRM using card '%s'\n", card);

    /* open the DRM device */
    ret = modeset_open(&drm_fd, card);
    if (ret)
        goto out_return;

    /* prepare all connectors and CRTCs */
    ret = modeset_prepare(drm_fd);
    if (ret)
        goto out_close;

    modeset_perform_modeset(drm_fd);

    return 0;

out_close:
    close(drm_fd);
out_return:
    if (ret) {
        errno = -ret;
        fprintf(stderr, "modeset failed with error %d: %m\n", errno);
    } else {
        fprintf(stderr, "exiting\n");
    }
    return ret;
}


void drm_display_buffer(void *src_buf)
{
    for (struct modeset_output *iter = output_list; iter; iter = iter->next)
    {
        struct modeset_buf *dst_buf = &iter->bufs[iter->front_buf ^ 1];
        memcpy(dst_buf->map, src_buf, dst_buf->size);
        modeset_draw_commit(drm_fd, iter);
    }
}
